跳到主要内容

Vue3 中配合 TypeScript 使用 Vuex

使用 TS 的问题

在 TypeScript 中使用 Vuex,必须编写自己的类型声明,这篇笔记主要记录如何使用(官方文档 TypeScript 部分根本就没有写)

State

定义一个 State 的类型 state.ts

export const state = {
counter: 0,
}

export type State = typeof state

这里需要导出类型信息和 state

Mutations

这里可以将这个 Mutations 定义成枚举类型 mutation-types.ts

export enum MutationTypes {
SET_COUNTER = 'SET_COUNTER',
}

现在我们已经定义了 Mutations 的名称,可以为每个 mutations(其实际类型)声明一个属性名。

Vuex 提供的 Mutation 函数,它接受 State 作为第一个参数,而 payload 作为第二个参数,

定义好枚举后,就可以开始定义真正的 mutation 了

mutations.ts:

import { MutationTree } from 'vuex'
import { MutationTypes } from './mutation-types'
import { State } from './state'

// 可以注意到,这里的 State 就自己定义的 State 类型
export type Mutations<S = State> = {
[MutationTypes.SET_COUNTER](state: S, payload: number): void
}

export const mutations: MutationTree<State> & Mutations = {
[MutationTypes.SET_COUNTER](state, payload: number) {
state.counter = payload
},
}

这个 mutation 可以用来修改上面 State 里面定义的 counter 属性,这里的 MutationTree<State> & Mutations 交叉属性可以保证 TypeScript 能正常编译,否则会抛出错误:

Type '{ SET_COUNTER(state: { counter: number; }, payload: number): void; }' is not assignable to type 'MutationTree<{ counter: number; }> & Mutations<{ counter: number; }>'.
Property '[MutationTypes.RESET_COUNTER]' is missing in type '{ SET_COUNTER(state: { counter: number; }, payload: number): void; }' but required in type 'Mutations<{ counter: number; }>'

它是一个泛型类型,与包一起提供。从它的名字来看,很明显,它有助于声明一个 MutationTree 类型(说白了就是另一种形式的继承接口..)

看它的实现:

vuex/types/index.d.ts:

export interface MutationTree<S> {
[key: string]: Mutation<S>;
}

它的作用就是兼容自己写的 State

Actions

对于这样一个简单的存储区,不需要 Actions 操作,但是为了说明操作的类型,让我们假设我们可以从某处获取 counter。

就像上面的 mutations 那样,这里也定义一个枚举 action-types.ts

export enum ActionTypes {
GET_COUTNER = 'GET_COUTNER',
}

actions.ts:

import { ActionTypes } from './action-types'

export const actions = {
[ActionTypes.GET_COUTNER]({ commit }) {
return new Promise((resolve) => {
setTimeout(() => {
const data = 256
commit(MutationTypes.SET_COUNTER, data)
resolve(data)
}, 500)
})
},
}

这里有一个简单的返回操作,它延时 500毫秒返回,但是这里也会像上面 mutations 那样报错:

所以这里也需要像上面那样使用交叉类型

import { ActionTree, ActionContext } from 'vuex'
import { State } from './state'
import { Mutations } from './mutations'
import { ActionTypes } from './action-types'
import { MutationTypes } from './mutation-types'

type AugmentedActionContext = {
commit<K extends keyof Mutations>(
key: K,
payload: Parameters<Mutations[K]>[1]
): ReturnType<Mutations[K]>
} & Omit<ActionContext<State, State>, 'commit'>

export interface Actions {
[ActionTypes.GET_COUTNER](
{ commit }: AugmentedActionContext,
payload: number
): Promise<number>
}

export const actions: ActionTree<State, State> & Actions = {
[ActionTypes.GET_COUTNER]({ commit }) {
return new Promise((resolve) => {
setTimeout(() => {
const data = 256
commit(MutationTypes.SET_COUNTER, data)
resolve(data)
}, 500)
})
},
}

Getters

Getters 也可以静态类型化。Getter 就像 mutation,本质上是一个接收状态作为第一个参数的函数。getters 的声明与 Actions 的声明没有太大区别。

getters.ts

import { GetterTree } from 'vuex'
import { State } from './state'

export type Getters = {
doubledCounter(state: State): number
}

export const getters: GetterTree<State, State> & Getters = {
doubledCounter: (state) => {
return state.counter * 2
},
}

拓充进全局类型 $store

现在把上面写的注册全局类型 $store

store.ts

import {
createStore,
Store as VuexStore,
CommitOptions,
DispatchOptions,
} from 'vuex'

import { State, state } from './state'
import { Getters, getters } from './getters'
import { Mutations, mutations } from './mutations'
import { Actions, actions } from './actions'

export const store = createStore({
state,
getters,
mutations,
actions,
})

export type Store = Omit<
VuexStore<State>,
'getters' | 'commit' | 'dispatch'
> & {
commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
key: K,
payload: P,
options?: CommitOptions
): ReturnType<Mutations[K]>
} & {
dispatch<K extends keyof Actions>(
key: K,
payload: Parameters<Actions[K]>[1],
options?: DispatchOptions
): ReturnType<Actions[K]>
} & {
getters: {
[K in keyof Getters]: ReturnType<Getters[K]>
}
}

我们已经到达了终点,剩下的就是全局 Vue 类型的扩展。(可以省略)

types/index.d.ts

import { Store } from '../store'

declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$store: Store
}
}

简单使用测试

这里有两种使用方式:Options API 和新的 Composition API

下面分别介绍:

Options API

<template>
<section>
<h2>Options API Component</h2>
<p>Counter: {{ counter }}, doubled counter: {{ counter }}</p>
<input v-model.number="counter" type="text" />
<button type="button" @click="resetCounter">Reset counter</button>
</section>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import { MutationTypes } from '../store/mutation-types'
import { ActionTypes } from '../store/action-types'

export default defineComponent({
name: 'OptionsAPIComponent',
computed: {
counter: {
get() {
return this.$store.state.counter
},
set(value: number) {
this.$store.commit(MutationTypes.SET_COUNTER, value)
},
},
doubledCounter() {
return this.$store.getters.doubledCounter
}
},
methods: {
resetCounter() {
this.$store.commit(MutationTypes.SET_COUNTER, 0)
},
async getCounter() {
const result = await this.$store.dispatch(ActionTypes.GET_COUTNER, 256)
},
},
})
</script>

Composition API

要在使用 Composition API 定义的组件中使用存储,我们必须通过钩子访问它,钩子只返回存储: useStore

export function useStore() {
return store as Store
}
<script lang="ts">
import { defineComponent, computed, h } from 'vue'
import { useStore } from '../store'
import { MutationTypes } from '../store/mutation-types'
import { ActionTypes } from '../store/action-types'

export default defineComponent({
name: 'CompositionAPIComponent',
setup(props, context) {
const store = useStore()

const counter = computed(() => store.state.counter)
const doubledCounter = computed(() => store.getters.doubledCounter)

function resetCounter() {
store.commit(MutationTypes.SET_COUNTER, 0)
}

async function getCounter() {
const result = await store.dispatch(ActionTypes.GET_COUTNER, 256)
}

return () =>
h('section', undefined, [
h('h2', undefined, 'Composition API Component'),
h('p', undefined, counter.value.toString()),
h('button', { type: 'button', onClick: resetCounter }, 'Reset coutner'),
])
},
})
</script>

模块化

import { createApp } from 'vue'
import { createStore } from 'vuex'

const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}

const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}

export default createStore({
modules: {
a: moduleA,
b: moduleB
}
})

在 Vue3 中使用 mapXXX 的问题

在 Vue3 中使用 mapGetters 时会发生报错,经排查,发现在 Vue3 中的 composition api 是无法使用 mapGetters的,因为它依赖于 this

具体参考这个 issue https://github.com/vuejs/vuex/issues/1948

所以下面这种写法用不了

const store = useStore();

const KeyGetters = computed(() => {
return {
...mapGetters([
'keyboard/isRecall'
// ...
])
};
});

得手动添加

const store = useStore();

const KeyGetters = computed(() => {
return {
isRecall: store.getters['keyboard/isRecall'],
selectKeys: store.getters['keyboard/selectKeys'],
pressedKeys: store.getters['keyboard/selectPressedKeys']
};
});

Reference

Vue 3, Vuex 4 Modules, Typescript(主要看这篇博客) 官方文档 TypeScript 支持 Vuex + TypeScript